import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;


public class FinalAI implements AI {
  private Battle game_state;

  // i want to commit
  private Heal_Weakest_Ally_Without_Threatening_Self heal_weakest_ally_without_threatening_self;
  private Heal_Weakest_Ally_Move_To_Least_Threatened heal_weakest_ally_move_to_least_threatened;
  private Run_Away run_away;
  private Attack_Best attack_best;
  private Move_To_Horizon_Closest_To_Weak_Enemy move_to_horizon_closest_to_weak_enemy; 
  private Is_There_A_Good_Target is_there_a_good_target;
  private Enemy_In_Attack_Range enemy_in_attack_range;
  private No_Ally_Attackers_Remaining no_ally_attackers_remaining;
  private Ally_Below_Half_And_Healer_Available rootNode;

  public FinalAI(Battle game_state) {
    this.game_state = game_state;

    heal_weakest_ally_without_threatening_self = new Heal_Weakest_Ally_Without_Threatening_Self();
    heal_weakest_ally_move_to_least_threatened = new Heal_Weakest_Ally_Move_To_Least_Threatened();
    run_away = new Run_Away();
    attack_best = new Attack_Best();
    move_to_horizon_closest_to_weak_enemy = new Move_To_Horizon_Closest_To_Weak_Enemy();

    is_there_a_good_target = new Is_There_A_Good_Target(attack_best, run_away);
    enemy_in_attack_range = new Enemy_In_Attack_Range(is_there_a_good_target, move_to_horizon_closest_to_weak_enemy);
    no_ally_attackers_remaining = new No_Ally_Attackers_Remaining(enemy_in_attack_range, heal_weakest_ally_without_threatening_self);
    rootNode = new Ally_Below_Half_And_Healer_Available(heal_weakest_ally_move_to_least_threatened, no_ally_attackers_remaining);
  }

  public Action decide(Battle game_state){
    this.game_state = game_state;
    DecisionTree.Actuator result = rootNode.makeDecision();
    return result.perform();
  }

  private static boolean is_a_healer(Combatant c){
    if(c.wlvl(WType.STAFF).is_at_least(WLvl.E)){
      return true;
    }
    return false;
  }

  public MapLocation best_loc_near_ally(Combatant healer, Combatant ally) {
    Map<MapLocation,Double> influence = game_state.influence_map_only_enemies(healer, Team.BLUE);
    MapLocation best = null;
    Set<MapLocation> possible_moves = game_state.move_range(healer).keySet();
    MapLocation ally_loc = game_state.loc(ally);
    for (Dir d : Dir.values()) {
      MapLocation candidate = game_state.neighbor(ally_loc, d);
      if (candidate != null && possible_moves.contains(candidate)) {
        if (best == null || influence.get(best) > influence.get(candidate)) best = candidate;
      }
    }
    return best;
  }

  public MapLocation best_loc_near_ally_not_threatened(Combatant healer, Combatant ally){
    Map<MapLocation,Double> influence = game_state.influence_map_only_enemies(healer, Team.BLUE);
    MapLocation best = null;
    Set<MapLocation> possible_moves = game_state.move_range(healer).keySet();
    MapLocation ally_loc = game_state.loc(ally);
    for (Dir d : Dir.values()) {
      MapLocation candidate = game_state.neighbor(ally_loc, d);
      if (candidate != null && possible_moves.contains(candidate)) {
        if (influence.get(candidate) == 0) best = candidate;
      }
    }
    return best;
  }

  private static Collection<Action> get_all_attack_actions(Battle game_state){
    Collection<Action> result = new LinkedList<Action>();
    Set<Combatant> redTeam = game_state.roster(Team.RED);
    for(Combatant curr: redTeam){
      if(game_state.hasnt_moved(curr) && !is_a_healer(curr)){
        Map<MapLocation,Path> move_range = game_state.move_range(curr);
        for(MapLocation destination: move_range.keySet()) {
          if(game_state.at(destination) != null && !game_state.at(destination).equals(curr))
            continue;
          Battle temp_context = game_state.clone();
          temp_context.move(curr,destination);
          for(Combatant target: temp_context) {
            if(curr.can_attack(temp_context,target)){
              result.add(Action.attack(curr,destination,target));
            }
          }
        }
      }
    }
    return result;
  }

  // Returns a rough average of attack values when the attacker attacks the defender.
  // Used for "goodness".  Again, it's a rough estimate.  Assumes no forests.
  private static double general_attack_value_avg(Combatant attacker, Combatant defender) {
    int SIDE_LENGTH = 5;
    Terrain[][] test_terrain = new Terrain[SIDE_LENGTH][SIDE_LENGTH];
    for (int i = 0; i < SIDE_LENGTH; i++) {
      for (int j = 0; j < SIDE_LENGTH; j++) test_terrain[i][j] = Terrain.PLAINS;
    }
    Battle test_battle = new Battle(test_terrain);
    test_battle.addBlue(SIDE_LENGTH/2 - 2, SIDE_LENGTH/2 - 1, attacker.clone());
    test_battle.addRed(SIDE_LENGTH/2 - 1, SIDE_LENGTH/2 - 1, defender.clone());

    double sum = 0.0;
    double count = 0.0;
    for (Action action : test_battle.successor_attacks()) {
      sum += action.calc_value(test_battle);
      count++;
    }
    return (sum / count);
  }

  // Calculates general_attack_value_avg against each enemy, returns one that attacker did best against.
  private static Combatant general_best_target(Battle game_state, Combatant attacker, Team enemy_team) {
    Combatant best_target = null;
    double best_value = 0.0;
    for (Combatant target : game_state.roster(enemy_team)) {
      double value = general_attack_value_avg(attacker, target);
      if (best_target == null || value > best_value) {
        best_value = value;
        best_target = target;
      }
    }
    return best_target;
  }

  // Given a unit and a set of locations (and Battle), finds the reachable location that's closest
  private static MapLocation closest_reachable(Battle game_state, Collection<MapLocation> locations, Combatant unit) {
    Collection<MapLocation> reachable = new HashSet<MapLocation>();
    Collection<MapLocation> move_range = game_state.move_range(unit).keySet();
    for (MapLocation m : locations)
      if (move_range.contains(m)) reachable.add(m);
    MapLocation src = game_state.loc(unit);
    MapLocation best_loc = null;
    int best_dist = 999;
    for (MapLocation loc : reachable) {
      int dist = game_state.distance(src, loc);
      if (best_loc == null ||dist < best_dist) {
        best_loc = loc;
        best_dist = dist;
      }
    }
    return best_loc;
  }

  private static MapLocation closest(Battle game_state, Collection<MapLocation> locations, MapLocation target) {
    MapLocation best_loc = null;
    int best_dist = 999;
    for (MapLocation loc : locations) {
      int dist = game_state.distance(target, loc);
      if (best_loc == null ||dist < best_dist) {
        best_loc = loc;
        best_dist = dist;
      }
    }
    return best_loc;
  }

  public static Collection<MapLocation> danger_zones(Battle game_state, Team enemy_team) {
    Collection<MapLocation> danger_zones = new HashSet<MapLocation>();
    for (Combatant enemy : game_state.roster(enemy_team))
      danger_zones.addAll(game_state.attack_range(enemy));
    return danger_zones;
  }

  // Returns all non-threatened squares that are one away from a threatened square.
  private static Collection<MapLocation> horizon_locations(Battle game_state, Combatant player, Team enemy_team) {
    Collection<MapLocation> result = new HashSet<MapLocation>();
    for (MapLocation src : danger_zones(game_state, enemy_team)) {
      for (Dir d : Dir.values()) {
        MapLocation loc = game_state.neighbor(src, d);
        if (loc != null) result.add(loc);
      }
    }
    return result;
  }

  private class Ally_Below_Half_And_Healer_Available extends DecisionTree.Decision{
    public Ally_Below_Half_And_Healer_Available(DecisionTree.Node yes_branch, DecisionTree.Node no_branch){
      super(yes_branch, no_branch);
    }

    public boolean decide(){
      boolean ally_below_half = false;
      boolean decision = false;
      Set<Combatant> redTeam = game_state.roster(Team.RED);

      for(Combatant ally: redTeam){
        if(ally.hp()/ally.max_hp() < 50 && !is_a_healer(ally)){
          ally_below_half = true;
          break;
        }
      }

      for(Combatant curr: redTeam){
        if (is_a_healer(curr)){
          if (game_state.hasnt_moved(curr)){
            if(ally_below_half){
              decision = true;
            }
          }
        }
      }
      return decision;
    }
  }

  private class No_Ally_Attackers_Remaining extends DecisionTree.Decision {
    public No_Ally_Attackers_Remaining (DecisionTree.Node yes_branch, DecisionTree.Node no_branch){
      super(yes_branch, no_branch);
    }

    public boolean decide(){
      Set<Combatant> redTeam = game_state.roster(Team.RED);
      boolean decision = false;
      for(Combatant ally: redTeam){
        if(game_state.hasnt_moved(ally) && !is_a_healer(ally)){
          decision = true;
        }
      }
      return decision;
    }
  }

  private class Enemy_In_Attack_Range extends DecisionTree.Decision{

    public Enemy_In_Attack_Range (DecisionTree.Node yes_branch, DecisionTree.Node no_branch){
      super(yes_branch, no_branch);
    }

    public boolean decide(){
      boolean decision = false;
      Set<Combatant> redTeam = game_state.roster(Team.RED);
      for(Combatant curr: redTeam){
        if(game_state.hasnt_moved(curr) && !is_a_healer(curr)){
          Map<MapLocation,Path> move_range = game_state.move_range(curr);
          for(MapLocation destination: move_range.keySet()) {
            if(game_state.at(destination) != null && !game_state.at(destination).equals(curr))
              continue;
            Battle temp_context = game_state.clone();
            temp_context.move(curr,destination);
            for(Combatant target: temp_context) {
              if(curr.can_attack(temp_context,target)){
                decision = true;
                break;
              }
            }
          }
        }
      }
      return decision;
    }
  }

  private class Is_There_A_Good_Target extends DecisionTree.Decision{
    public Is_There_A_Good_Target (DecisionTree.Node yes_branch, DecisionTree.Node no_branch){
      super(yes_branch, no_branch);
    }

    public boolean decide(){
      double MINIMUM = 0.0;
      Collection<Action> result = get_all_attack_actions(game_state);
      for(Action A: result) if (A.calc_value(game_state) > MINIMUM) return true;
      return false;
    }
  }

  private class Heal_Weakest_Ally_Move_To_Least_Threatened extends DecisionTree.Actuator{

    public Action perform() {
      Set<Combatant> redTeam = game_state.roster(Team.RED);
      Combatant healer = null;
      Combatant weakest_ally = null;
      MapLocation weakest_ally_loc = null;
      int most_health_missing = 0;
      Action result = null;
      for(Combatant curr: redTeam){
        if(is_a_healer(curr) && game_state.hasnt_moved(curr)){
          healer = curr;
        }
      }

      for(Combatant curr: redTeam){
        if((curr.max_hp() - curr.hp()) > most_health_missing && !healer.equals(curr)){
          Map<MapLocation,Path> move_range = game_state.move_range(curr);
          for(MapLocation destination: move_range.keySet()) {
            if(game_state.at(destination) != null && !game_state.at(destination).equals(curr))
              continue;
            Battle temp_context = game_state.clone();
            temp_context.move(healer,destination);
            if(healer.can_use_staff(temp_context,curr)){
              if (weakest_ally_loc == null || temp_context.location_isnt_threatened(destination, Team.RED)){
                weakest_ally = curr;
                most_health_missing = (curr.max_hp() - curr.hp());
                weakest_ally_loc = destination;
              }
            }
          }
        }
      }

      if(healer != null && weakest_ally != null){
        MapLocation best_loc_near_weakest_ally = best_loc_near_ally(healer, weakest_ally);
        if (best_loc_near_weakest_ally != null) result = Action.staff(healer, best_loc_near_weakest_ally, weakest_ally);
        else result = Action.wait(healer, game_state.loc(healer)); //shouldn't really ever get here
      }
      return result;
    }
  }

  private class Heal_Weakest_Ally_Without_Threatening_Self extends DecisionTree.Actuator{

    public Action perform(){
      Action result = null;

      Set<Combatant> redTeam = game_state.roster(Team.RED);
      Combatant healer = null;

      // finds the healer
      for(Combatant curr: redTeam){
        if(is_a_healer(curr) && game_state.hasnt_moved(curr)){
          healer = curr;
        }
      }

      // generates a list of all the hurt allies
      Collection<Combatant> hurt_allies = new HashSet<Combatant>();
      for(Combatant curr: redTeam){
        if(!curr.equals(healer) && (curr.max_hp() - curr.hp()) > 0){
          // curr_ally can be null
          MapLocation curr_ally = best_loc_near_ally_not_threatened(healer, curr);
          if(curr_ally != null)
            hurt_allies.add(curr);
        }
      }

      // finds the most hurt ally
      Combatant most_hurt_ally = null;
      int highest_damage = 0;
      for(Combatant ally: hurt_allies){
        if((ally.max_hp() - ally.hp()) > highest_damage){
          most_hurt_ally = ally;
        }
      }

      MapLocation move_healer_here = null;
      if(most_hurt_ally != null){
        move_healer_here = best_loc_near_ally_not_threatened(healer, most_hurt_ally);
        result = Action.staff(healer, move_healer_here, most_hurt_ally);
      }
      // if there is not hurt ally that is has a space not threatened then retreat to a non threatened square
      // if no retreat then wait
      else {
        Map<MapLocation,Double> influence = game_state.influence_map_only_enemies(healer, Team.BLUE);
        MapLocation best = null;
        Set<MapLocation> possible_moves = game_state.move_range(healer).keySet();
        for(MapLocation can_go_here: possible_moves){
          if(influence.get(can_go_here) == 0){
            best = can_go_here;
            break;
          }
        }

        if(best != null)
          result = Action.wait(healer, best);

        // if no safe square then wait where you are (cause you screwed XD)
        else
          result = Action.wait(healer, game_state.loc(healer));
      }
      return result;
    }
  }

  class Run_Away extends DecisionTree.Actuator {
    public Action perform() {

      Action run_away = null;

      Set<Combatant> red_team = game_state.roster(Team.RED);
      Map<MapLocation,Double> influence;
      Combatant most_threatened = null;
      MapLocation least_influenced = null;
      boolean injured = false;
      //check for injured combatants
      for(Combatant curr : red_team)
      {
        if(curr.hp() < curr.max_hp())
        {
          injured = true;
          break;
        }
      }

      if(injured)
      {
        for(Combatant curr : red_team)
        {
          if(most_threatened == null)
            most_threatened =curr;
          else{
            if(curr.hp() < curr.max_hp() && curr.hp() < most_threatened.hp())
              most_threatened = curr;
            else if(curr.hp() == curr.max_hp()){
              //randomly choose 
            }

          }

        }
        //influence = game_state.influence_map_only_enemies(most_threatened, Team.BLUE );
      }else{
        //if no injured check for least defense
        for(Combatant curr : red_team){
          if(most_threatened ==null)
          {
            most_threatened = curr;
          }else{
            if(curr.defense() < most_threatened.defense() )
            {
              most_threatened = curr;
            }else if(curr.defense()  == most_threatened.defense())
            {
              //randomly choose
            }
          }
        }
      }

      influence = game_state.influence_map_only_enemies(most_threatened, Team.BLUE );
      //move to least threatened square on map 
      for(MapLocation m: influence.keySet()){
        if(least_influenced == null)
          least_influenced = m;
        else{
          if(influence.get(m) < influence.get(least_influenced))
            least_influenced = m;
        }
      }
      game_state.move(most_threatened, least_influenced);

      return run_away;
    }
  }

  // Picks the one attack of remaining attacks that has highest "value", as computed by Action.calc_value()
  class Attack_Best extends DecisionTree.Actuator {
    public Action perform() {
      Collection<Action> possible_actions = game_state.successor_attacks();
      Action best_action = null;
      double best_value = 0.0;
      for (Action action : possible_actions) {
        double value = action.calc_value(game_state);
        if (best_action == null || value > best_value) {
          best_action = action;
          best_value = value;
        }
      }
      return best_action;
    }
  }

  class Move_To_Horizon_Closest_To_Weak_Enemy extends DecisionTree.Actuator {
    public Action perform() {
      Team ENEMY_TEAM = Team.BLUE;
      // Currently, just pick random player who can still move
      Combatant player = game_state.movable_set().iterator().next();

      // Find all squares that are just out of range of the enemy
      Collection<MapLocation> horizons = horizon_locations(game_state, player, ENEMY_TEAM);

      // Find the enemy that this player would (probably) do best against
      Combatant favorable_enemy = general_best_target(game_state,player,ENEMY_TEAM);

      // Find the square among the "horizon" squares that the favored enemy is closest to
      MapLocation horizon_target = closest_reachable(game_state,horizons,favorable_enemy);

      // If not ending in threatened square, move as close to that target square as possible
      MapLocation final_target = closest(game_state, game_state.move_range(player).keySet(), horizon_target);
      
      if (final_target != null) return Action.wait(player, final_target);
      return Action.wait(player,game_state.loc(player));
    }
  }

  /*
	public Set<MapLocation> getHorizon() {
		Set<MapLocation> result = new Set<MapLocation>();

	}
   */
}